経歴書ドラフトPDF生成の非同期タスク化(ADR-0020)#469
Conversation
GitHub連携後のドラフトPDF生成を、連携とは別トリガーの独立した非同期 タスクにする。連携ボタンとドラフト生成ボタンを分離し、生成はバック グラウンドで完了・通知される(既存の非同期タスク基盤・ポーリングを再利用)。 - 新TaskType RESUME_DRAFT / ハンドラ / run_task.py(LLM→PDF検証→課金→保存) - 新テーブル resume_draft_cache に payload のみ最小永続化(resumes には触れない) DL時に build_resume_pdf で再レンダリング(バイナリをDBに持たない) - エンドポイント: 旧同期 POST /resume-draft/pdf を廃止し run(202)/status/pdf(GET) に置換 - 課金をタスク側へ移設(PDF成功後のみ課金/LLM・パース失敗は課金/PDF失敗は課金なし を維持) - モデルはユーザー選択中のものを使用。フロントは enqueue→ポーリング→DL へ改修 - ADR-0020 追加(0018 の「何も永続化しない」を最小永続化へ refine) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 11 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThis PR migrates resume draft PDF generation from a synchronous, single-request LLM call into an asynchronous task-based flow. It adds a ChangesAsync resume draft generation
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Frontend as useResumeDraftPdf
participant Router as agent router
participant Cache as resume_draft_cache
participant Worker as run_resume_draft_task
participant LLM as LLM/PDF builder
participant Billing as credit_service
Frontend->>Router: POST /resume-draft/run
Router->>Router: ensure_can_use_model, build_draft_source
Router->>Cache: get_or_create + set status=pending
Router->>Worker: dispatch background task
Router-->>Frontend: 202 TaskAcceptedResponse
loop polling
Frontend->>Router: GET /resume-draft/status
Router->>Cache: read status
Router-->>Frontend: TaskStatusResponse
end
Worker->>Cache: set status=processing
Worker->>LLM: run_resume_draft (generate)
alt LLM/parse failure
Worker->>Billing: charge consumed tokens
Worker->>Cache: raise NonRetryableError (dead_letter)
else success
Worker->>LLM: build_resume_pdf (validate)
alt PDF failure
Worker->>Cache: raise NonRetryableError (no charge)
else PDF valid
Worker->>Billing: record_chat_usage (final charge)
Worker->>Cache: write result, status=completed
end
end
Frontend->>Router: GET /resume-draft/pdf
Router->>Cache: read result (completed)
Router-->>Frontend: streamed PDF
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
backend/tests/test_resume_draft_api.py (1)
85-160: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winMissing test: re-enqueue guard when a draft task is already in-progress.
The router's
try_reset_to_pendingshort-circuits and returns the existingcache.status(not"pending") instead of re-dispatching when a task is already pending/processing. This idempotency path prevents duplicate dispatch/re-billing but isn't covered by any test in this file.♻️ Suggested test addition
def test_resume_draft_run_returns_existing_status_when_already_pending(client: TestClient) -> None: """既に pending/processing 中なら再ディスパッチせず現在のステータスを返す。""" headers = auth_header(client, github_id=1) _seed_link_data(client._db_session) user = client._db_session.query(User).filter_by(username="testuser").one() client._db_session.add(ResumeDraftCache(user_id=user.id, status="processing")) client._db_session.commit() res = client.post("/api/agent/resume-draft/run", json={"model": "haiku"}, headers=headers) assert res.status_code == 202 assert res.json()["status"] == "processing"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/tests/test_resume_draft_api.py` around lines 85 - 160, Add a test for the idempotent re-enqueue guard in the resume draft flow: when an existing ResumeDraftCache is already in "pending" or "processing", the request should not dispatch again and should return the current cache status instead of "pending". Extend backend/tests/test_resume_draft_api.py with a case around test_resume_draft_run_enqueues and try_reset_to_pending that seeds link data, inserts a ResumeDraftCache in processing state, calls /api/agent/resume-draft/run, and asserts the 202 response echoes "processing" from the response payload.web/src/hooks/useResumeDraftPdf.ts (1)
92-105: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winStale
previewUrlnot cleared when re-generating.Unlike the prior synchronous flow,
generate()no longer resetspreviewUrlbefore enqueueing a new task, so an old preview can remain visible/stale until the new one completes. Self-recovers onceloadPreviewruns, but could confuse users mid-regeneration.🩹 Suggested fix
const generate = async () => { if (generating) return; setGenerating(true); setError(null); + updatePreviewUrl(null); try { await startResumeDraft(model); startPolling();🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/hooks/useResumeDraftPdf.ts` around lines 92 - 105, The generate flow in useResumeDraftPdf should clear any existing previewUrl before starting a new draft generation, since the async enqueue/polling path can leave a stale preview visible until loadPreview updates it. Update generate() to reset the preview state alongside setGenerating/setError before calling startResumeDraft(model), so the UI doesn’t show an outdated preview during regeneration.web/src/api/agent.ts (1)
25-53: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueADR-0020 に更新してください
この非同期ドラフトフローの JSDoc は ADR-0018 ではなく ADR-0020 を参照するように揃えてください。関連するソースコメントも同じ番号に合わせると、生成される API ドキュメントとのずれもなくなります。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/api/agent.ts` around lines 25 - 53, The JSDoc references in the resume draft async flow are still pointing to ADR-0018, so update the comments on startResumeDraft, getResumeDraftStatus, and fetchResumeDraftPdfBlobUrl to reference ADR-0020 instead. Keep the surrounding descriptions the same, and make sure any related source comments for this flow use the same ADR number so the generated API docs stay consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.claude/rules/backend/agent.md:
- Around line 54-74: `run_resume_draft_task()` の課金と `resume_draft_cache`
への結果保存が別トランザクションになっており、さらに rerun 時に `dead_letter` を `pending`
に戻すため、`record_chat_usage()`
後に失敗すると再実行で二重課金される可能性があります。`run_resume_draft_task()`, `record_chat_usage()`,
`resume_draft_cache`, `dead_letter`
の再投入経路を見直し、課金済みマーカーを永続化するか、課金記録と結果保存を同一の原子操作にまとめてから、この billing flow
を安全とみなすよう修正してください。
In `@backend/app/services/agent/resume_draft/run_task.py`:
- Around line 105-126: The post-billing write-back in run_task() is still
retryable, so failures after record_chat_usage() can rerun the task and risk
double-charging or inconsistent cache state. Update the second session_factory
block that loads ResumeDraftCacheRepository and writes result/status so any
exception there is wrapped as NonRetryableError, matching the billing failure
handling, and keep the cache update path non-retryable after the charge has been
committed.
---
Nitpick comments:
In `@backend/tests/test_resume_draft_api.py`:
- Around line 85-160: Add a test for the idempotent re-enqueue guard in the
resume draft flow: when an existing ResumeDraftCache is already in "pending" or
"processing", the request should not dispatch again and should return the
current cache status instead of "pending". Extend
backend/tests/test_resume_draft_api.py with a case around
test_resume_draft_run_enqueues and try_reset_to_pending that seeds link data,
inserts a ResumeDraftCache in processing state, calls
/api/agent/resume-draft/run, and asserts the 202 response echoes "processing"
from the response payload.
In `@web/src/api/agent.ts`:
- Around line 25-53: The JSDoc references in the resume draft async flow are
still pointing to ADR-0018, so update the comments on startResumeDraft,
getResumeDraftStatus, and fetchResumeDraftPdfBlobUrl to reference ADR-0020
instead. Keep the surrounding descriptions the same, and make sure any related
source comments for this flow use the same ADR number so the generated API docs
stay consistent.
In `@web/src/hooks/useResumeDraftPdf.ts`:
- Around line 92-105: The generate flow in useResumeDraftPdf should clear any
existing previewUrl before starting a new draft generation, since the async
enqueue/polling path can leave a stale preview visible until loadPreview updates
it. Update generate() to reset the preview state alongside
setGenerating/setError before calling startResumeDraft(model), so the UI doesn’t
show an outdated preview during regeneration.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: b1c56f8e-c984-477b-9471-66dabdae96ce
📒 Files selected for processing (22)
.claude/rules/backend/agent.mdbackend/alembic_migrations/versions/0047_add_resume_draft_cache_table.pybackend/app/messages.jsonbackend/app/models/__init__.pybackend/app/models/cache.pybackend/app/repositories/resume_draft.pybackend/app/routers/agent.pybackend/app/services/agent/resume_draft/run_task.pybackend/app/services/tasks/base.pybackend/app/services/tasks/handlers/__init__.pybackend/app/services/tasks/handlers/resume_draft.pybackend/tests/test_resume_draft_api.pybackend/tests/test_worker/test_resume_draft.pydocs/adr/0020-async-resume-draft-generation.mddocs/adr/README.mdweb/e2e/github-link.spec.tsweb/src/api/agent.tsweb/src/api/generated.tsweb/src/api/paths.tsweb/src/constants/messages.tsweb/src/hooks/useResumeDraftPdf.test.tsweb/src/hooks/useResumeDraftPdf.ts
課金(record_chat_usage)と結果保存を同一トランザクションで確定し、 「課金済みだが結果未保存」からの再実行による二重課金を構造的に防ぐ。 加えてフェーズA に completed 冪等ガードを追加し、原子 commit 後・ack 前の クラッシュによる Cloud Tasks 再配信での再課金を防ぐ。回帰テストを追加。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QVqj6qCo31rXwKASxB5V2Z
概要
GitHub 連携後の経歴書ドラフト PDF 生成(ADR-0018)を、連携とは別トリガーの独立した非同期タスクに改修する。従来は同期エンドポイントで LLM→PDF 生成→レスポンスで PDF をそのまま返しており、生成中はブラウザが待たされ、タブを閉じると生成が無駄になっていた。
連携ボタンとドラフト生成ボタンを分離し、ドラフト生成はバックグラウンドで完了・通知される(既存の非同期タスク基盤・ポーリングを再利用)。モデルはユーザーが選択中のものを使う。
主な変更
バックエンド
TaskType.RESUME_DRAFT+ ハンドラ +run_task.py(連携データ取得→LLM→PDF レンダリング検証→課金→保存)resume_draft_cache(マイグレーション0047)に payload(JSON) のみ最小永続化。resumes/Resumeには一切触れない。ダウンロード時にbuild_resume_pdfで再レンダリング(DB にバイナリを持たない)POST /api/agent/resume-draft/pdfを廃止し、POST .../run(202) /GET .../status/GET .../pdfに置換NonRetryableErrorで dead_letter 化(リトライ再課金を防止)フロント
useResumeDraftPdfを enqueue→useTaskPolling→ダウンロードへ改修(返り値の形は不変のためダッシュボードは変更なし)。マウント時に進行中タスクを検知したらポーリング復帰。Blob URL リーク防止は維持ドキュメント
agent.mdを更新テスト
make cigreen(backend lint〈lint-tdd・lint-adr-index 含む〉+ test 、web vitest 372 件、build)test_worker/test_resume_draft.py(成功=completed+課金/パース失敗=課金あり/PDF 失敗=課金なし/未整備=NonRetryable)、test_resume_draft_api.pyを非同期契約へ全面改修(enqueue 202 / status / download / 402 / 409 / 422)useResumeDraftPdf.test.tsを非同期フローへ改修、E2E に「連携→ドラフト生成→プレビュー」1 本追加備考
POST /api/agent/resume-draft/pdfを削除(GET 版に置換)。web/src/api/generated.tsを再生成・同梱ADD TABLEのみ)。実 libSQL 適用は CIsmoke-backendで検証origin/main起点で非同期化分のみを対象にしている🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Tests